package net.cocotea.sqltoy.lambda.factory.condition;

import net.cocotea.sqltoy.lambda.common.SqlCharter;
import net.cocotea.sqltoy.lambda.common.SqlKeyword;
import net.cocotea.sqltoy.lambda.factory.condition.opt.*;
import net.cocotea.sqltoy.lambda.func.SFunction;

import java.util.*;
import java.util.function.Consumer;

public abstract class AbstractLambdaCondition<T> implements ICondition<T> {

    private static final long serialVersionUID = -1847868328993435664L;

    protected AbstractLambdaCondition() {
    }

    protected Map<String, Object> namesAndValuesMap = null;
    protected StringJoiner whereSql = null;
    protected boolean isOr = false;

    @Override
    public AbstractLambdaCondition<T> eq(SFunction<T, ?> function, Object value) {
        Eq<T> eq = new Eq<>(function, value);
        putNamesAndValues(eq.valueName(), value);
        putWhereSql(eq.fragment());
        return this;
    }

    @Override
    public AbstractLambdaCondition<T> neq(SFunction<T, ?> fn, Object value) {
        Neq<T> neq = new Neq<>(fn, value);
        putNamesAndValues(neq.valueName(), value);
        putWhereSql(neq.fragment());
        return this;
    }

    @Override
    public AbstractLambdaCondition<T> gt(SFunction<T, ?> fn, Object value) {
        Gt<T> gt = new Gt<>(fn, value);
        putNamesAndValues(gt.valueName(), value);
        putWhereSql(gt.fragment());
        return this;
    }

    @Override
    public AbstractLambdaCondition<T> gte(SFunction<T, ?> fn, Object value) {
        Gte<T> gte = new Gte<>(fn, value);
        putNamesAndValues(gte.valueName(), value);
        putWhereSql(gte.fragment());
        return this;
    }

    @Override
    public AbstractLambdaCondition<T> lt(SFunction<T, ?> fn, Object value) {
        Lt<T> lt = new Lt<>(fn, value);
        putNamesAndValues(lt.valueName(), value);
        putWhereSql(lt.fragment());
        return this;
    }

    @Override
    public AbstractLambdaCondition<T> lte(SFunction<T, ?> fn, Object value) {
        Lte<T> lte = new Lte<>(fn, value);
        putNamesAndValues(lte.valueName(), value);
        putWhereSql(lte.fragment());
        return this;
    }

    @Override
    public AbstractLambdaCondition<T> like(SFunction<T, ?> fn, String value) {
        Like<T> like = new Like<>(fn, value);
        putNamesAndValues(like.valueName(), value);
        putWhereSql(like.fragment());
        return this;
    }

    @Override
    public AbstractLambdaCondition<T> notLike(SFunction<T, ?> fn, String value) {
        NotLike<T> notLike = new NotLike<>(fn, value);
        putNamesAndValues(notLike.valueName(), value);
        putWhereSql(notLike.fragment());
        return this;
    }

    @Override
    public AbstractLambdaCondition<T> leftLike(SFunction<T, ?> fn, String value) {
        LeftLike<T> leftLike = new LeftLike<>(fn, value);
        putNamesAndValues(leftLike.valueName(), (SqlCharter.PERCENT + value));
        putWhereSql(leftLike.fragment());
        return this;
    }

    @Override
    public AbstractLambdaCondition<T> rightLike(SFunction<T, ?> fn, String value) {
        RightLike<T> rightLike = new RightLike<>(fn, value);
        putNamesAndValues(rightLike.valueName(), (value + SqlCharter.PERCENT));
        putWhereSql(rightLike.fragment());
        return this;
    }

    @Override
    public AbstractLambdaCondition<T> in(SFunction<T, ?> fn, Collection<?> values) {
        In<T> in = new In<>(fn, values);
        putNamesAndValues(in.valueName(), values);
        putWhereSql(in.fragment());
        return this;
    }

    @Override
    public AbstractLambdaCondition<T> notIn(SFunction<T, ?> fn, Collection<?> values) {
        NotIn<T> notIn = new NotIn<>(fn, values);
        putNamesAndValues(notIn.valueName(), values);
        putWhereSql(notIn.fragment());
        return this;
    }

    @Override
    public AbstractLambdaCondition<T> between(SFunction<T, ?> fn, Object value1, Object value2) {
        if (value1 == null || value2 == null) {
            throw new IllegalArgumentException("value1 and value2 cannot be null");
        }
        Between<T> between = new Between<>(fn, Arrays.asList(value1, value2));
        putNamesAndValues(between.valuedName1(), value1);
        putNamesAndValues(between.valuedName2(), value2);
        putWhereSql(between.fragment());
        return this;
    }

    @Override
    public AbstractLambdaCondition<T> and() {
        this.isOr = false;
        return this;
    }

    @Override
    public AbstractLambdaCondition<T> nested(Consumer<ICondition<T>> action) {
        LambdaCondition<T> condition = new LambdaCondition<>();
        action.accept(condition);
        if (isOr) {
            whereSql.add(SqlKeyword.OR);
        } else {
            whereSql.add(SqlKeyword.AND);
        }

        String fragment = condition.getFragment();
        int index = fragment.indexOf(SqlKeyword.AND);
        if (index > 0) {
            String start = fragment.substring(0, index);
            String end = fragment.substring(index + SqlKeyword.AND.length());
            fragment = start + end;
        }

        String inner = SqlCharter.LEFT_PARENTHESIS + fragment + SqlCharter.RIGHT_PARENTHESIS;
        this.sqlJoin(inner);
        this.putNamesAndValues(condition.namesAndValuesMap);
        return this;
    }

    @Override
    public AbstractLambdaCondition<T> or() {
        this.isOr = true;
        return this;
    }

    private void putWhereSql(String fragment) {
        if (whereSql == null) {
            whereSql = new StringJoiner(SqlCharter.SPACE);
        }
        String prefix = SqlCharter.WELL_NO + SqlCharter.LEFT_SQUARE_BRACKETS;
        if (isOr) {
            whereSql.add(prefix + SqlKeyword.OR);
        } else {
            whereSql.add(prefix + SqlKeyword.AND);
        }
        whereSql.add(fragment + SqlCharter.RIGHT_SQUARE_BRACKETS);
    }

    private void putNamesAndValues(String name, Object value) {
        if (namesAndValuesMap == null) {
            namesAndValuesMap = new HashMap<>();
        }
        namesAndValuesMap.put(name, value);
    }

    private void putNamesAndValues(Map<String, Object> map) {
        if (namesAndValuesMap == null) {
            namesAndValuesMap = new HashMap<>();
        }
        namesAndValuesMap.putAll(map);
    }

    private void sqlJoin(String fragment) {
        if (whereSql == null) {
            whereSql = new StringJoiner(SqlCharter.SPACE);
        }
        whereSql.add(fragment);
    }

    public String getFragment() {
        if (whereSql == null) {
            return SqlCharter.EMPTY;
        }
        return whereSql.toString();
    }
}
